/*******************************************************************************
 * Copyright (c) 2010, 2017 Oak Ridge National Laboratory and others.
 *
 * This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 * 
 * SPDX-License-Identifier: EPL-2.0
 * 
 * Contributors:
 *    Xihui Chen - initial API and implementation
 *    Max Hohenegger - Bug 418168
 ******************************************************************************/
package org.eclipse.nebula.visualization.xygraph.dataprovider;

import java.util.Calendar;
import java.util.Iterator;

import org.eclipse.swt.widgets.Display;

/**
 * Provides data to a trace.
 * 
 * @author Xihui Chen
 *
 */
public class CircularBufferDataProvider extends AbstractDataProvider {

	public enum UpdateMode {
		X_OR_Y("X or Y"), X_AND_Y("X AND Y"), X("X"), Y("Y"), TRIGGER("Trigger");

		private UpdateMode(String description) {
			this.description = description;
		}

		private String description;

		@Override
		public String toString() {
			return description;
		}

		public static String[] stringValues() {
			String[] sv = new String[values().length];
			int i = 0;
			for (UpdateMode p : values())
				sv[i++] = p.toString();
			return sv;
		}
	}

	public enum PlotMode {
		LAST_N("Plot last n pts."), N_STOP("Plot n pts & stop.");

		private PlotMode(String description) {
			this.description = description;
		}

		private String description;

		@Override
		public String toString() {
			return description;
		}

		public static String[] stringValues() {
			String[] sv = new String[values().length];
			int i = 0;
			for (PlotMode p : values())
				sv[i++] = p.toString();
			return sv;
		}
	}

	private CircularBuffer<ISample> traceData;

	private double currentXData;

	private double currentYData;

	private long currentYDataTimestamp;

	private boolean currentXDataChanged = false;

	private boolean currentYDataChanged = false;

	// private boolean currentYDataTimestampChanged = false;

	private double[] currentXDataArray = new double[] {};

	private double[] currentYDataArray = new double[] {};

	private boolean currentXDataArrayChanged = false;

	private boolean currentYDataArrayChanged = false;

	private boolean xAxisDateEnabled = false;

	private int updateDelay = 0;
	private boolean duringDelay = false;

	private boolean concatenate_data = true;

	private UpdateMode updateMode = UpdateMode.X_AND_Y;

	private PlotMode plotMode = PlotMode.LAST_N;

	private Runnable fireUpdate;

	public CircularBufferDataProvider(boolean chronological) {
		super(chronological);
		traceData = new CircularBuffer<ISample>(100);
		fireUpdate = new Runnable() {
			public void run() {
				for (IDataProviderListener listener : listeners) {
					listener.dataChanged(CircularBufferDataProvider.this);
				}
				duringDelay = false;
			}
		};
	}

	/**
	 * @param currentXData
	 *            the currentXData to set
	 */
	public synchronized void setCurrentXData(double newValue) {
		this.currentXData = newValue;
		currentXDataChanged = true;
		tryToAddDataPoint();
	}

	/**
	 * Set current YData. It will automatically make timestamp disabled.
	 * 
	 * @param currentYData
	 *            the currentYData to set
	 */
	public synchronized void setCurrentYData(double newValue) {
		this.currentYData = newValue;
		currentYDataChanged = true;
		xAxisDateEnabled = false;
		// if(!xAxisDateEnabled|| (xAxisDateEnabled &&
		// currentYDataTimestampChanged))
		tryToAddDataPoint();
	}

	public synchronized void addSample(ISample sample) {
		if (traceData.size() == traceData.getBufferSize() && plotMode == PlotMode.N_STOP)
			return;
		traceData.add(sample);
		fireDataChange();
	}

	/**
	 * Set the time stamp of currrent YData
	 * 
	 * @param timestamp
	 *            timestamp of Y data in milliseconds.
	 */
	public synchronized void setCurrentYDataTimestamp(long timestamp) {
		setXAxisDateEnabled(true);
		this.currentYDataTimestamp = timestamp;
		// currentYDataTimestampChanged = true;
		if (currentYDataChanged)
			tryToAddDataPoint();
	}

	/**
	 * Set current YData and its timestamp when the new value generated.
	 * 
	 * @param currentYData
	 *            the currentYData to set
	 * @param timestamp
	 *            timestamp of Y data in milliseconds.
	 */
	public synchronized void setCurrentYData(double newValue, long timestamp) {
		setXAxisDateEnabled(true);
		this.currentYData = newValue;
		currentYDataChanged = true;
		this.currentYDataTimestamp = timestamp;
		// currentYDataTimestampChanged = true;
		tryToAddDataPoint();
	}

	/**
	 * Try to add a new data point to trace data. Whether it will be added or
	 * not is up to the update mode.
	 */
	private void tryToAddDataPoint() {
		if (traceData.size() == traceData.getBufferSize() && plotMode == PlotMode.N_STOP)
			return;
		switch (updateMode) {
		case X_OR_Y:
			if ((chronological && currentYDataChanged)
					|| (!chronological && (currentXDataChanged || currentYDataChanged)))
				addDataPoint();
			break;
		case X_AND_Y:
			if ((chronological && currentYDataChanged)
					|| (!chronological && (currentXDataChanged && currentYDataChanged)))
				addDataPoint();
			break;
		case X:
			if ((chronological && currentYDataChanged) || (!chronological && currentXDataChanged))
				addDataPoint();
			break;
		case Y:
			if (currentYDataChanged)
				addDataPoint();
			break;
		case TRIGGER:

		default:
			break;
		}
	}

	/**
	 * add a new data point to trace data.
	 */
	private void addDataPoint() {
		double newXValue;
		if (!concatenate_data)
			traceData.clear();
		if (chronological) {
			if (xAxisDateEnabled) {
				if (updateMode != UpdateMode.TRIGGER)
					newXValue = currentYDataTimestamp;
				else
					newXValue = Calendar.getInstance().getTimeInMillis();
			} else {
				if (traceData.size() == 0)
					newXValue = 0;
				else
					newXValue = traceData.getTail().getXValue() + 1;
			}
		} else {
			newXValue = currentXData;
		}
		traceData.add(new Sample(newXValue, currentYData));
		currentXDataChanged = false;
		currentYDataChanged = false;
		// currentYDataTimestampChanged = false;
		fireDataChange();
	}

	/**
	 * @param currentXData
	 *            the currentXData to set
	 */
	public synchronized void setCurrentXDataArray(double[] newValue) {
		this.currentXDataArray = newValue;
		currentXDataArrayChanged = true;
		tryToAddDataArray();
	}

	/**
	 * @param currentXData
	 *            the currentXData to set
	 */
	public synchronized void setCurrentYDataArray(double[] newValue) {
		this.currentYDataArray = newValue;
		currentYDataArrayChanged = true;
		tryToAddDataArray();
	}

	/**
	 * Try to add a new data array to trace data. Whether it will be added or
	 * not is up to the update mode.
	 */
	private void tryToAddDataArray() {
		if (traceData.size() == traceData.getBufferSize() && plotMode == PlotMode.N_STOP)
			return;
		switch (updateMode) {
		case X_OR_Y:
			if ((chronological && currentYDataArrayChanged)
					|| (!chronological && (currentXDataArrayChanged || currentYDataArrayChanged)))
				addDataArray();
			break;
		case X_AND_Y:
			if ((chronological && currentYDataArrayChanged)
					|| (!chronological && (currentXDataArrayChanged && currentYDataArrayChanged)))
				addDataArray();
			break;
		case X:
			if ((chronological && currentYDataArrayChanged) || (!chronological && currentXDataArrayChanged))
				addDataArray();
			break;
		case Y:
			if (currentYDataArrayChanged)
				addDataArray();
			break;
		case TRIGGER:
		default:
			break;
		}
	}

	/**
	 * add a new data point to trace data.
	 */
	private void addDataArray() {
		if (!concatenate_data)
			traceData.clear();

		if (chronological) {
			double[] newXValueArray;
			newXValueArray = new double[currentYDataArray.length];
			if (traceData.size() == 0)
				for (int i = 0; i < currentYDataArray.length; i++) {
					newXValueArray[i] = i;
				}
			else
				for (int i = 1; i < currentYDataArray.length + 1; i++) {
					newXValueArray[i - 1] = traceData.getTail().getXValue() + i;
				}
			for (int i = 0; i < Math.min(traceData.getBufferSize(),
					Math.min(newXValueArray.length, currentYDataArray.length)); i++) {
				traceData.add(new Sample(newXValueArray[i], currentYDataArray[i]));
			}
		} else {
			// newXValueArray = currentXDataArray;

			// if the data array size is longer than buffer size,
			// just ignore the tail data.
			for (int i = 0; i < Math.min(traceData.getBufferSize(),
					Math.min(currentXDataArray.length, currentYDataArray.length)); i++) {
				traceData.add(new Sample(currentXDataArray[i], currentYDataArray[i]));
			}
		}

		currentXDataArrayChanged = false;
		currentYDataArrayChanged = false;
		// currentYDataTimestampChanged = false;
		fireDataChange();
	}

	/**
	 * Clear all data on in the data provider.
	 */
	public synchronized void clearTrace() {
		traceData.clear();
		currentXDataArray = new double[] {};
		currentYDataArray = new double[] {};
		currentXDataChanged = false;
		currentYDataChanged = false;
		currentXDataArrayChanged = false;
		currentYDataArrayChanged = false;
		fireDataChange();
	}

	public Iterator<ISample> iterator() {
		return traceData.iterator();
	}

	/**
	 * @param bufferSize
	 *            the bufferSize to set
	 */
	public synchronized void setBufferSize(int bufferSize) {
		traceData.setBufferSize(bufferSize, false);
	}

	/**
	 * @param updateMode
	 *            the updateMode to set
	 */
	public void setUpdateMode(UpdateMode updateMode) {
		this.updateMode = updateMode;
	}

	/**
	 * @return the update mode.
	 */
	public UpdateMode getUpdateMode() {
		return updateMode;
	}

	/**
	 * In TRIGGER update mode, the trace data could be updated by this method
	 * 
	 * @param triggerValue
	 *            the triggerValue to set
	 */
	public void triggerUpdate() {
		// do not update if no new data was added, otherwise, it will add (0,0)
		// which is not a real sample.
		if (traceData.size() == 0 && !(currentYDataChanged || currentYDataArrayChanged))
			return;
		if (currentYDataArray.length > 0)
			addDataArray();
		else
			addDataPoint();
	}

	/**
	 * @param plotMode
	 *            the plotMode to set
	 */
	public void setPlotMode(PlotMode plotMode) {
		this.plotMode = plotMode;
	}

	@Override
	public ISample getSample(int index) {
		return traceData.getElement(index);
	}

	@Override
	public int getSize() {
		return traceData.size();
	}

	/**
	 * If xAxisDateEnable is true, you will need to use
	 * {@link #setCurrentYData(double, long)} or
	 * {@link #setCurrentYDataTimestamp(long)} to set the time stamp of ydata.
	 * This flag will be automatically enabled when either of these two methods
	 * were called. The default value is false.
	 * 
	 * @param xAxisDateEnabled
	 *            the xAxisDateEnabled to set
	 */
	public void setXAxisDateEnabled(boolean xAxisDateEnabled) {
		if (this.xAxisDateEnabled != xAxisDateEnabled) {
			this.xAxisDateEnabled = xAxisDateEnabled;
			clearTrace();
		}
	}

	/**
	 * @param updateDelay
	 *            Delay in milliseconds between plot updates. This may help to
	 *            reduce CPU usage. The default value is 0ms.
	 */
	public synchronized void setUpdateDelay(int updateDelay) {
		this.updateDelay = updateDelay;
	}

	@Override
	protected synchronized void fireDataChange() {
		if (updateDelay > 0) {
			if (!duringDelay) {
				Display.getCurrent().timerExec(updateDelay, fireUpdate);
				duringDelay = true;
			}
		} else
			super.fireDataChange();
	}

	public void setConcatenate_data(boolean concatenate_data) {
		this.concatenate_data = concatenate_data;
	}

	public boolean isConcatenate_data() {
		return concatenate_data;
	}

}
